#include "Subsystems/SubsystemWeather.h"
#include "Subsystems/SubsystemTerrain.h"
#include "Subsystems/SubsystemBodies.h"
#include "Subsystems/SubsystemExplosions.h"
#include "Subsystems/SubsystemTimeOfDay.h"
#include "Subsystems/SubsystemGameInfo.h"
#include "Subsystems/BlockBehaviors/SubsystemSnowBlockBehavior.h"

#include "Utils/Utils.h"
#include "Network/PackageManager.h"
#include "Network/Packages/SubsystemSkyPackage.h"
#include "Network/Packages/SubsystemWeatherPackage.h"

#include "Engine/Vector2.hpp"

#include "Components/ComponentBody.h"
#include "Components/ComponentOnFire.h"
#include "Components/ComponentCreature.h"
#include "Components/ComponentHealth.h"

#include "Log.h"
#include "GameRandom.hpp"
#include "GameSingleton.h"

#include "Blocks/WaterBlock.h"
#include "Blocks/SnowBlock.h"
#include "Blocks/IceBlock.h"
#include "Blocks/AirBlock.h"


namespace PocketSurvival
{

    static const int32_t lightValuesMoonless[6] {0, 3, 6, 9, 12, 15};
    static const int32_t lightValuesNormal[6] {3, 5, 8, 10, 13, 15};

    void SubsystemWeather::makeLightningStrike(const Vector3 &targetPosition)
    {
        netMakeLightningStrike(targetPosition);
        GameSingleton::packageManager->addSendPackageToQueue(std::make_shared<SubsystemSkyPackage>(targetPosition));
    }

    void SubsystemWeather::netMakeLightningStrike(const Vector3 &targetPosition)
    {
        std::vector<ComponentBody *> bodyVec;
        subsystemBodies->findBodiesAroundPoint(Vector2(targetPosition.posX, targetPosition.posZ), 4.0f, bodyVec);
        for (ComponentBody *body : bodyVec)
        {
            Vector3 position;
            body->getPosition(position);

            if (position.posY > targetPosition.posY - 1.5f && Vector2::Distance(Vector2(position.posX, position.posZ), Vector2(targetPosition.posX, targetPosition.posZ)) < 4.0f)
            {
                body->componentCreature->componentHealth->componentOnFire->setOnFile(entt::null, GameSingleton::gameRandom->getFloat(12.0f, 15.0f));
            }
        }
        int32_t x = Terrain::ToCell(targetPosition.posX);
        int32_t num3 = Terrain::ToCell(targetPosition.posY);
        int32_t z = Terrain::ToCell(targetPosition.posZ);
        float pressure = (GameSingleton::gameRandom->getFloat(0.0f, 1.0f) < 0.2f) ? 39 : 19;
        subsystemExplosions->addExplosion(x, num3 + 1, z, pressure, false);
    }

    void SubsystemWeather::manualLightingStrike(const Vector3 &position, const Vector3 &direction)
    {
        int32_t num = Terrain::ToCell(position.posX + direction.posX * 32.0f);
        int32_t num2 = Terrain::ToCell(position.posZ + direction.posZ * 32.0f);
        std::shared_ptr<Vector3> vector = nullptr;
        for (int32_t i = 0; i < 300; i++)
        {
            int32_t num3 = GameSingleton::gameRandom->getInt(-8, 8);
            int32_t num4 = GameSingleton::gameRandom->getInt(-8, 8);
            int32_t num5 = num + num3;
            int32_t num6 = num2 + num4;
            int32_t num7 = subsystemTerrain->calculateTopmostCellHeight(num5, num6);
            if (vector == nullptr || num7 > vector->posY)
            {
                vector = std::make_shared<Vector3>(num5, num7, num6);
            }
        }
        if (vector != nullptr)
        {
            makeLightningStrike(*vector);
        }
    }

    PrecipitationShaftInfo SubsystemWeather::getPrecipitationShaftInfo(int32_t x, int32_t z)
    {
        int32_t shaftValue = m_subsystemTerrain->getShaftValue(x, z);
        int32_t seasonalTemperature = m_subsystemTerrain->getSeasonalTemperature(shaftValue);
        int32_t num = Terrain::ExtractTopHeight(shaftValue);
        PrecipitationShaftInfo result;
        if (IsPlaceFrozen(seasonalTemperature, num))
        {
            result.Intensity = globalPrecipitationIntensity;
            result.Type = PrecipitationType::Snow;
            result.YLimit = num + 1;
            return result;
        }
        int32_t seasonalHumidity = m_subsystemTerrain->getSeasonalHumidity(shaftValue);
        if (seasonalTemperature <= 8 || seasonalHumidity >= 8)
        {
            result.Intensity = globalPrecipitationIntensity;
            result.Type = PrecipitationType::Rain;
            result.YLimit = num + 1;
            return result;
        }
        result.Intensity = 0.0f;
        result.Type = PrecipitationType::Rain;
        result.YLimit = num + 1;
        return result;
    }

    void SubsystemWeather::updateLightAndViewParameters()
    {
        skylightIntensity = calculateLightIntensity(timeOfDay->getTimeOfDay());
        int32_t index = (int)MathUtils::Round(MathUtils::Lerp(0.0f, 5.0f, skylightIntensity));
        if (moonPhase == 4)
        {
            skylightValue = lightValuesMoonless[index];
        }
        else
        {
            skylightValue = lightValuesNormal[index];
        }
    }

    float SubsystemWeather::calculateLightIntensity(float timeOfDay)
    {
        if (timeOfDay <= 0.2f || timeOfDay > 0.8f)
        {
            return 0.0f;
        }
        if (timeOfDay > 0.2f && timeOfDay <= 0.3f)
        {
            return (timeOfDay - 0.2f) / (71.0f / (226.0f * (float)MathUtils::GetPI()));
        }
        if (timeOfDay > 0.3f && timeOfDay <= 0.7f)
        {
            return 1.0f;
        }
        return 1.0f - (timeOfDay - 0.7f) / 0.100000024f;
    }

    void SubsystemWeather::freezeThawAndDepositSnow(TerrainChunk *chunk)
    {
        for (int32_t i = 0; i < 16; i++)
        {
            for (int32_t j = 0; j < 16; j++)
            {
                if (GameSingleton::gameRandom->getBool(0.25f))
                {
                    continue;
                }
                int32_t topHeightFast = chunk->getTopHeightFast(i, j);
                int32_t cellValueFast = chunk->getCellValueFast(i, topHeightFast, j);
                int32_t num = Terrain::ExtractContents(cellValueFast);
                int32_t num2 = chunk->origin.posX + i;
                int32_t num3 = topHeightFast;
                int32_t num4 = chunk->origin.posY + j;
                PrecipitationShaftInfo precipitationShaftInfo = getPrecipitationShaftInfo(num2, num4);
                if (precipitationShaftInfo.Type == PrecipitationType::Snow)
                {
                    if (num == WaterBlock::Index())
                    {
                        int32_t cellContents  = m_subsystemTerrain->getCellContents(num2 + 1, num3, num4);
                        int32_t cellContents2 = m_subsystemTerrain->getCellContents(num2 - 1, num3, num4);
                        int32_t cellContents3 = m_subsystemTerrain->getCellContents(num2, num3, num4 - 1);
                        int32_t cellContents4 = m_subsystemTerrain->getCellContents(num2, num3, num4 + 1);
                        bool num5 = cellContents   != WaterBlock::Index() && cellContents  != AirBlock::Index();
                        bool flag = cellContents2  != WaterBlock::Index() && cellContents2 != AirBlock::Index();
                        bool flag2 = cellContents3 != WaterBlock::Index() && cellContents3 != AirBlock::Index();
                        bool flag3 = cellContents4 != WaterBlock::Index() && cellContents4 != AirBlock::Index();
                        if (num5 | flag | flag2 | flag3)
                        {
                            m_subsystemTerrain->changeCell(num2, num3, num4, Terrain::MakeBlockValue(IceBlock::Index()));
                        }
                    }
                    else if (precipitationShaftInfo.Intensity > 0.5f && m_subsystemSnowBlockBehavior->canSupportSnow(cellValueFast) && (num != IceBlock::Index() || ShaftHasSnowOnIce(num2, num4)) && num3 + 1 < 255)
                    {
                        m_subsystemTerrain->changeCell(num2, num3 + 1, num4, Terrain::MakeBlockValue(SnowBlock::Index()));
                    }
                }
                else
                {
                    switch (num)
                    {
                    case SnowBlock::Index():
                        m_subsystemTerrain->destroyCell(0, num2, num3, num4, 0, true);
                        break;
                    case IceBlock::Index():
                        m_subsystemTerrain->destroyCell(0, num2, num3, num4, 0, false);
                        break;
                    }
                }
            }
        }
    }

    void SubsystemWeather::update(float dt)
    {
        moonPhase = (MathUtils::Floor<int32_t>(timeOfDay->getDay() - 0.5 + 5.0) % 8 + 8) % 8;
        updateLightAndViewParameters();

        if (gameInfo->totalElapsedGameTime > weatherEndTime)
        {
            constexpr float baseTime = 60.0f;

            weatherStartTime = gameInfo->totalElapsedGameTime + baseTime * GameSingleton::gameRandom->getFloat(5.0f, 45.0f);
            weatherEndTime = weatherStartTime + baseTime * GameSingleton::gameRandom->getFloat(3.0f, 6.0f);
            lightningIntensity = (GameSingleton::gameRandom->getBool(0.5f) ? GameSingleton::gameRandom->getFloat(0.33f, 1.0f) : 0.0f);
            GameSingleton::packageManager->addSendPackageToQueue(std::make_shared<SubsystemWeatherPackage>(weatherStartTime, weatherEndTime, lightningIntensity));
        }

        float num = MathUtils::Max<float>(0.0f, MathUtils::Min<float>(gameInfo->totalElapsedGameTime - weatherStartTime, weatherEndTime - gameInfo->totalElapsedGameTime));
        globalPrecipitationIntensity = gameInfo->areWeatherEffectsEnabled ? MathUtils::Saturate(num * 0.04f) : 0.0f;

        if(timeOfDay->periodicGameTimeEvent(0.5, 0))
        {
            if(m_waitHandleChunkQueue.size() <= 0)
            {
                std::vector<Point2> terrainCoordsVec;
                m_subsystemTerrain->getAllTerrainChunkCoords(terrainCoordsVec);
                for(auto it = terrainCoordsVec.begin(); it != terrainCoordsVec.end(); ++it)
                {
                    m_waitHandleChunkQueue.push(*it);
                }
            }
            else
            {
                Point2 coords = m_waitHandleChunkQueue.front();
                m_waitHandleChunkQueue.pop();
                TerrainChunk *chunk = m_subsystemTerrain->getTerrainChunk(coords.posX, coords.posY);
                if(chunk != nullptr)
                {
                    if (globalPrecipitationIntensity == 1.0f && timeOfDay->periodicGameTimeEvent(1.0, 0.0))
                    {
                        if(GameSingleton::gameRandom->getBool(0.01f))
                        {
                            Vector3 lightningPos(chunk->origin.posX, 0, chunk->origin.posY);
                            for (int32_t j = 0; j < 16; j++)
                            {
                                for (int32_t k = 0; k < 16; k++)
                                {
                                    int32_t topHeight = chunk->getTopHeightFast(j, k);
                                    if (topHeight > lightningPos.posY)
                                    {
                                        lightningPos.posX = chunk->origin.posX + j;
                                        lightningPos.posY = topHeight;
                                        lightningPos.posZ = chunk->origin.posY + k;
                                    }
                                }
                            }
                            makeLightningStrike(lightningPos);
                            return;
                        }
                    }
                    freezeThawAndDepositSnow(chunk);
                }
            }
        }
    }

    void SubsystemWeather::load(const nlohmann::json &json)
    {
        Utils::GetJsonValue<double>(json, weatherStartTime, "WeatherStartTime", 0.0);
        Utils::GetJsonValue<double>(json, weatherEndTime, "WeatherEndTime", 0.0);
        Utils::GetJsonValue<float>(json, lightningIntensity, "LightningIntensity", 0.0f);

        subsystemBodies = GameSingleton::singletonPtr->getSingleton<SubsystemBodies>();
        subsystemTerrain = GameSingleton::singletonPtr->getSingleton<SubsystemTerrain>();
        subsystemExplosions = GameSingleton::singletonPtr->getSingleton<SubsystemExplosions>();
        timeOfDay = GameSingleton::singletonPtr->getSingleton<SubsystemTimeOfDay>();
        gameInfo = GameSingleton::singletonPtr->getSingleton<SubsystemGameInfo>();

        m_subsystemTerrain = GameSingleton::singletonPtr->getSingleton<SubsystemTerrain>();
        m_subsystemSnowBlockBehavior = GameSingleton::singletonPtr->getSingleton<SubsystemSnowBlockBehavior>();
    }

    void SubsystemWeather::save(nlohmann::json &json)
    {
        json["WeatherStartTime"] = weatherStartTime;
        json["WeatherEndTime"] = weatherEndTime;
        json["LightningIntensity"] = lightningIntensity;
    }

    void SubsystemWeather::saveToMsgJson(nlohmann::json &jsonData)
    {
        MSG_VALUE_SET(jsonData, "WeatherStartTime", "double", weatherStartTime);
        MSG_VALUE_SET(jsonData, "WeatherEndTime", "double", weatherEndTime);
        MSG_VALUE_SET(jsonData, "LightningIntensity", "float", lightningIntensity);
    }



    static const std::string subsystemName = "Weather";
    const std::string &SubsystemWeather::getName() const
    {
        return subsystemName;
    }
    SubUpdateType SubsystemWeather::getUpdateType()
    {
        return SubUpdateType::MultiThreadUpdate;
    }

}